Skip to content

feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407)#3413

Draft
os-zhuang wants to merge 2 commits into
mainfrom
claude/update-record-silent-field-strip-l77rag
Draft

feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407)#3413
os-zhuang wants to merge 2 commits into
mainfrom
claude/update-record-silent-field-strip-l77rag

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3407(从 #3356 拆出的可观测性缺口)。

问题

update_record 节点无条件返回 success,即使请求写入的字段被数据层合法丢弃(静态 readonly #2948 / 条件 readonlyWhen #3042)。数据层的 warn 只落在服务端 logger,不进流程运行的步骤日志 —— 作者在 run trace 里只看到一条 3ms 的 success,这正是 #3356 里审批流 stage 回写整链失效被掩盖的原因。

方案拍板

采用 issue 中的**方向 1(引擎结构化回传)**的回调变体,未选方向 3(日志路由):日志路由需要把 per-run logger 一路透传进引擎与剥离函数,侵入更深,且节点只能拿到非结构化文本、拿不到字段名列表。

调研中确认的两个范围收窄:

  • FLS 写门已非静默:plugin-securitydetectForbiddenWrites 现在直接抛 PermissionDeniedError(fail-closed),会表现为 success: false,不属于本单的静默剥离;
  • insert 不剥离 readonly(INSERT 豁免是既有语义,FLS 抛错),所以 create_record 今天不会产生 warning —— 但按 issue 要求对称接线,未来 insert 若新增静默剥离会自动浮现,不会重新变哑。

实现

spec(契约,@objectstack/spec)

  • data/data-engine.zod.ts:新增 DroppedFieldsEventSchema —— { object, fields, reason: 'readonly' | 'readonly_when' },Zod-first、可发布进 json-schema manifest(已随构建更新);
  • contracts/data-engine.ts:新增 WriteObservabilityOptions(onFieldsDropped 监听器),以交叉类型挂在 IDataEngine.insert/update 的 options 形参上。刻意不进可序列化的 Zod options schema:函数在 JSON Schema 不可表示(会触发 manifest ratchet 失败),也不可能跨 RPC(Virtual Data Engine)边界 —— 这是进程内通道,与 IDataEngine 本身同层(TS 契约层);
  • packages/core 的镜像契约同步(该文件当前无引用方,仅保持一致)。

objectql(engine.update())

  • 在全部 4 个剥离点(单 id / bulk × readonly / readonlyWhen)之后,对比剥离前后键集并回调 onFieldsDropped。对比是精确的:剥离函数在无丢弃时返回同一引用,有丢弃时返回浅拷贝;
  • 监听器抛错不影响写入(catch + warn);
  • system 上下文跳过 readonly 剥离、因此不产生事件 —— 行为与之前一致;
  • insert() 出于签名对称接受该 option,但今天不触发(见上)。

service-automation

  • NodeExecutionResult / StepLogEntry 新增 advisory 的 warnings?: string[],executeNode 在成功/失败步骤条目上透传,随 run history 持久化(compactStepLogForHistory 展开透传,无需改动);
  • update_record / create_record:每个剥离事件生成一条 warning(点名字段与原因,readonlyWhen 措辞覆盖 bulk 的「≥1 matched row」语义),并在 output 暴露结构化的 droppedFields(下游节点可读 {<nodeId>.droppedFields});
  • success 语义不变 —— 剥离依旧是合法语义,只是不再沉默。

测试

  • packages/objectql/src/engine.test.ts 新增 6 例:单 id readonly / 单 id readonlyWhen / bulk readonlyWhen / system 豁免不报 / 无丢弃不报 / 监听器抛错不破坏写入;
  • packages/services/service-automation/src/builtin/crud-dropped-fields.test.ts 新增 5 例:步骤 warning(成功状态保持)、droppedFields 输出变量、无剥离无 warning、create_record 对称接线;
  • 全量回归:spec 6818 ✓ / objectql 1042 ✓ / service-automation 347 ✓ / core 386 ✓;json-schema.manifest.jsonapi-surface.json 已随构建脚本再生(纯增量)。

含 changeset(spec / objectql / service-automation 各 minor)与 content/docs/kernel/contracts/data-engine.mdx 的契约文档更新。

🤖 Generated with Claude Code

https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77


Generated by Claude Code

…ings (#3407)

update_record reported an unconditional success even when the data layer
legally stripped the requested write fields (static readonly #2948,
conditional readonlyWhen #3042) — the only trace was a server-side logger
warn, invisible in the flow run trace, which is how #3356's approval stage
write-backs failed behind a clean 3ms success.

- spec: new DroppedFieldsEventSchema ({object, fields, reason}) in
  data/data-engine.zod.ts (Zod-first, JSON-representable, published to the
  schema manifest) + a WriteObservabilityOptions mixin (onFieldsDropped
  listener) on IDataEngine.insert/update option params in
  contracts/data-engine.ts. The listener is TS-contract-level and
  in-process only — a function is unrepresentable in JSON Schema and never
  crosses the RPC boundary. Core's mirror contract kept in sync.
- objectql: engine.update() reports each strip pass's dropped keys +
  reason through options.onFieldsDropped at all four strip sites
  (single-id + bulk x readonly + readonly_when), diffing before/after key
  sets (exact: strips return the same reference when nothing dropped). A
  throwing listener never breaks the write. System-context writes skip the
  readonly strip and report nothing, as before. insert() accepts the
  option for signature symmetry but strips nothing today (INSERT is
  readonly-exempt; FLS write denial throws).
- service-automation: NodeExecutionResult and StepLogEntry gain advisory
  warnings?: string[] (persisted through run history; success semantics
  unchanged). update_record / create_record attach one warning per strip
  event naming the dropped fields and expose a structured droppedFields
  output ({<nodeId>.droppedFields}) for downstream nodes.

Design per issue #3407 direction 1 (engine structured feedback) — chosen
over run-scoped logger routing (direction 3): shallower intrusion, and the
node gets a structured field list instead of log text. FLS is not wired:
its write gate now throws (fail-closed) instead of stripping, so it is no
longer a silent-drop case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 22, 2026 10:14pm

Request Review

Three rerun_failed_jobs rounds on the runs queued during the 2026-07-22
GitHub-hosted-runner outage all failed in seconds with no logs (HTTP 404),
including after GitHub resolved the incident at 22:09 UTC. Fresh push to
mint fresh workflow runs instead of re-running the poisoned run records.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaofCZbsPTHE2oJedvKd77

Copy link
Copy Markdown
Contributor Author

CI 红灯是仓库级 Actions 故障,与本 PR 代码无关 — 诊断如下:

  • 本仓所有分支的所有 workflow run(含其他 PR 分支 claude/field-value-shape-contract-1z4pilclaude/inverse-drift-undeclared-props-5qdonq)从 20:59 UTC 起全部在 3-4 秒内 failure,job 日志 HTTP 404(runner 从未真正启动),check run output 为空;
  • 时间窗与 GitHub 官方事故吻合(githubstatus:GitHub-hosted runners 降级,20:47 确认、22:09 宣布解决),但 22:11 UTC 空提交触发的全新 run 依旧秒死 — 属残余效应或组织级问题(若持续,建议查一下 org 的 Actions 计费/spending limit);
  • 已重踢 4 轮(3 次 rerun_failed_jobs + 1 次空提交新 run)均无效,停止无效重试。

本地全量测试是绿的(spec 6818 / objectql 1042 / service-automation 347 / core 386)。我会继续定时自检,GitHub 恢复后自动重踢并汇报。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service-automation: update_record reports success when written fields are silently stripped — no observability for dropped writes (split from #3356)

2 participants